Hilt 实战 | 创建应用级别 CoroutineScope
协程最佳实践
https://developer.android.google.cn/kotlin/coroutines/coroutines-best-practices
手动依赖项注入
class MyRepository(private val externalScope: CoroutineScope) { /* ... */ }
class MyApplication : Application() {
// 应用中任何类都可以通过 applicationContext 访问应用级别作用域的类型
val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
val myRepository = MyRepository(applicationScope)
}
手动
https://developer.android.google.cn/training/dependency-injection/manual
由于在 Android 中没有可靠的方法来获取 Application 销毁的时机,并且应用级别的作用域以及任何正在执行的任务都将同应用进程的结束一同销毁,也意味着您无需手动调用 applicationScope.cancel()。
处理如何构造确切类型的逻辑; 持有容器级别作用域的类型实例; 返回限定作用域或未限定作用域的类型实例。
class ApplicationDiContainer {
val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
val myRepository = MyRepository(applicationScope)
}
class MyApplication : Application() {
val applicationDiContainer = ApplicationDiContainer()
}
说明: 容器类永远返回被限定作用域的类型的相同实例,并且永远返回未被限定作用域的类型的不同实例。将类型的作用域限定到容器类中成本很高,这是因为在组件销毁之前,被限定作用域的对象将一直存在于内存中,所以仅在真正需要限定作用域的场景使用。
成本很高 https://developer.android.google.cn/training/dependency-injection/hilt-android#component-scopes
class ApplicationDiContainer {
// 限定作用域类型。永远返回相同的实例
val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
// 未限定作用域类型。永远返回不同实例
fun getMyRepository(): MyRepository {
return MyRepository(applicationScope)
}
}
在应用中使用 Hilt
在 Hilt 中,可以通过使用注解在编译期生成 ApplicationDiContainer 的内容 (甚至更多)!并且 Hilt 除 Application 类外,还为大部分 Android Framework 类提供了容器。
在您的应用中配置 Hilt 并且创建 Application 类的容器,可以在 Application 类中使用 @HiltAndroidApp 注解。
@HiltAndroidApp
class MyApplication : Application()
Android Framework 类 https://developer.android.google.cn/training/dependency-injection/hilt-android#generated-components
此时,应用 DI 容器已经可以使用了。我们只需要让 Hilt 知道如何提供不同类型的实例。
说明: 在 Hilt 中,容器类被引用为组件。与 Application 关联的组件被称为 SingletonComponent。请参阅 —— Hilt 提供的组件列表: https://developer.android.google.cn/training/dependency-injection/hilt-android#generated-components
构造方法注入
@Singleton // 限定作用域为 SingletonComponent
class MyRepository @Inject constructor(
private val externalScope: CoroutineScope
) {
/* ... */
}
此时,Hilt 还不知道如何提供满足要求的 CoroutineScope 依赖项,因为我们还没有告诉 Hilt 该如何处理。 接下来的部分将展示如何让 Hilt 知道应该传递哪些依赖项。
说明: Hilt 提供了多种注解,来实现将类型的作用域限定到各种 Hilt 的现有组件中。请参阅 —— Hilt 提供的组件列表: https://developer.android.google.cn/training/dependency-injection/hilt-android#generated-components
绑定
绑定是 Hilt 中的一个常见术语,它表明了 Hilt 所知的如何提供类型的实例作为依赖项的信息。我们可以说,上文的代码片段就是使用 @Inject 在 Hilt 中添加了绑定。
未限定作用域的类型的绑定 (假如上文的 MyRepository 代码去掉 @Singleton 就是一个例子),在任何 Hilt 组件中都可用。将绑定的作用域限定到一个组件,例如被 @Singleton 注解的 MyRepository,可以在当前作用域的组件以及该层级以下的组件中使用。
组件层次结构 https://developer.android.google.cn/training/dependency-injection/hilt-android#component-hierarchy
通过模块提供类型
@InstallIn(SingletonComponent::class)
@Module
object CoroutinesScopesModule {
@Singleton // 永远提供相同实例
@Provides
fun providesCoroutineScope(): CoroutineScope {
// 当提供 CoroutineScope 实例时,执行如下代码
return CoroutineScope(SupervisorJob() + Dispatchers.Default)
}
}
使用模块
https://developer.android.google.cn/training/dependency-injection/hilt-android#hilt-modules
@Provides 注解的方法同时被 @Singleton 注解,让 Hilt 总是返回相同的 CoroutineScope 实例。这是因为任何需要遵循应用生命周期的任务都应该使用遵循应用生命周期的 CoroutineScope 的同一实例创建。
被 @InstallIn 注解的 Hilt 模块,表明该绑定被装载到哪个 Hilt 组件中 (包含该组件层级以下的组件)。在我们的案例中,被限定作用域到 SingletonComponent 上的 MyRepository,需要应用级别的 CoroutineScope,该绑定同样需要被装载到 SingletonComponent 中。
如果使用 Hilt 的行话,可以说成我们添加了一个 CoroutineScope 绑定,至此,Hilt 就知道如何提供 CoroutineScope 实例了。
然而,上述代码片段仍可以优化。协程中硬编码 Dispatcher 不是良好的实现,我们需要注入它们使得这些 Dispatcher 可配置并且易于测试。基于之前的代码,我们可以创建一个新的 Hilt 模块,让它知道为每种情况需要注入哪个 Dispatcher: main、default 还是 IO。
协程中硬编码 Dispatcher 不是良好的实现 https://developer.android.google.cn/kotlin/coroutines/coroutines-best-practices#inject-dispatchers
提供 CoroutineDispatcher 的实现
我们需要提供相同类型 CoroutineDispatcher 的不同实现。换句话说就是,我们需要相同类型的不同绑定。
我们可以使用限定符来让 Hilt 知道每种情况需要使用哪种绑定或者实现。限定符只是您和 Hilt 之间用来标识特定绑定的注解。让我们为每一种 CoroutineDispatcher 的实现创建一个限定符:
// CoroutinesQualifiers.kt 文件
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class DefaultDispatcher
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class IoDispatcher
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class MainDispatcher
@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class MainImmediateDispatcher
限定符 https://developer.android.google.cn/training/dependency-injection/hilt-android#multiple-bindings
@InstallIn(SingletonComponent::class)
@Module
object CoroutinesDispatchersModule {
@DefaultDispatcher
@Provides
fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
@IoDispatcher
@Provides
fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
@MainDispatcher
@Provides
fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
@MainImmediateDispatcher
@Provides
fun providesMainImmediateDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate
}
提供应用级别作用域的 CoroutineScope
为了从我们之前的应用级别作用域的 CoroutineScope 代码中摆脱硬编码 CoroutineDispatcher,我们需要注入 Hilt 提供的默认 Dispatcher。为此,我们可以传入我们想要注入的类型: CoroutineDispatcher,在提供应用级别 CoroutineScope 的方法中使用对应的限定符 @DefaultDispatcher 作为依赖项。
@InstallIn(SingletonComponent::class)
@Module
object CoroutinesScopesModule {
@Singleton
@Provides
fun providesCoroutineScope(
@DefaultDispatcher defaultDispatcher: CoroutineDispatcher
): CoroutineScope = CoroutineScope(SupervisorJob() + defaultDispatcher)
}
由于 Hilt 对 CoroutineDispatcher 类型具有多个绑定,因此当 CoroutineDispatcher 用作依赖项时,我们使用 @DefaultDispatcher 注解消除它的歧义。
应用级别作用域限定符
虽然我们目前不需要 CoroutineScope 的多个绑定 (未来我们可能需要像 UserCoroutineScope 这样的协程作用域),但是向应用级别 CoroutineScope 添加限定符可以提高其作为依赖项注入时的可读性。
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class ApplicationScope
@InstallIn(SingletonComponent::class)
@Module
object CoroutinesScopesModule {
@Singleton
@ApplicationScope
@Provides
fun providesCoroutineScope(
@DefaultDispatcher defaultDispatcher: CoroutineDispatcher
): CoroutineScope = CoroutineScope(SupervisorJob() + defaultDispatcher)
}
@Singleton
class MyRepository @Inject constructor(
@ApplicationScope private val externalScope: CoroutineScope
) { /* ... */ }
在插桩测试中替换 Dispatcher
如上所述,我们应该注入 Dispatcher 使测试更容易并可以完全控制发生的事情。对于插桩测试,我们希望 Espresso 等待协程结束。
我们可以利用 AsyncTask API 来替代使用 Espresso 空闲资源创建自定义 CoroutineDispatcher,来等待协程的结束。即使 AsyncTask 已经在 Android API 30 中被弃用,但 Espresso 会 hook 到其线程池中来检查空闲情况。因此,任何应该在后台执行的协程都可以在 AsyncTask 的线程池中执行。
AsyncTask https://developer.android.google.cn/reference/android/os/AsyncTask Espresso 空闲资源 https://developer.android.google.cn/training/testing/espresso/idling-resource
在测试中可以使用 Hilt TestInstallIn API 让 Hilt 提供一个类型的不同实现。这与上文提供不同 Dispatcher 类似,我们可以在 androidTest 包下创建一个新文件,来提供不同的 Dispatcher 实现。
// androidTest/projectPath/TestCoroutinesDispatchersMouule.kt 文件
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [CoroutinesDispatchersModule::class]
)
@Module
object TestCoroutinesDispatchersModule {
@DefaultDispatcher
@Provides
fun providesDefaultDispatcher(): CoroutineDispatcher =
AsyncTask.THREAD_POOL_EXECUTOR.asCoroutineDispatcher()
@IoDispatcher
@Provides
fun providesIoDispatcher(): CoroutineDispatcher =
AsyncTask.THREAD_POOL_EXECUTOR.asCoroutineDispatcher()
@MainDispatcher
@Provides
fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
}
警告: 这其实是通过 hack 的方式实现的,虽然不值得炫耀,然而由于 Espresso 目前没有办法知道 CoroutineDispatcher 是否处于空闲状态 (issue 链接),所以协程并不能与其完美的集成。因为 Espresso 不是使用空闲资源来检查该 executor 是否空闲,而是通过消息队列中是否有内容的方式,所以 AsyncTask.THREAD_POOL_EXECUTOR 是目前最佳的替代方案。也正是这些原因,使得它相对于诸如 IdlingThreadPoolExecutor 之类来说是一个更优解,并且非常不幸的是,当由于协程被编译成状态机而被挂起时,IdlingThreadPoolExecutor 会认为线程池是空闲的。
issue 链接 https://github.com/Kotlin/kotlinx.coroutines/issues/242 IdlingThreadPoolExecutor https://developer.android.google.cn/reference/androidx/test/espresso/idling/concurrent/IdlingThreadPoolExecutor
更多关于测试的信息,请参阅 Hilt 测试指南:
通过本文,您已经了解到如何使用 Hilt 创建一个应用级别的 CoroutineScope 作为依赖项注入,如何注入不同的 CoroutineDispatcher 实例,以及如何在测试中替换它们的实现。
欢迎您通过下方二维码向我们提交反馈,或分享您喜欢的内容、发现的问题。您的反馈对我们非常重要,感谢您的支持!
免费中文系列课程下载
系统地学习使用 Kotlin 进行 Android 开发
☟ 即刻了解课程详情 ☟
推荐阅读